/*
 * File     : X86_64UnixNativeCallFactory.java
 * Created  : 21 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.call;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.library.DynamicLibrary;
import com.googlecode.dni.internal.structure.StructureRecord;
import com.googlecode.dni.library.Function.LastErrorBehaviour;
import com.googlecode.dni.library.IllegalLibraryException;
import com.googlecode.dni.type.Pointer;


/**
 * <p>
 *  Implementation of {@link NativeCallFactory} that produces native methods for
 *  calling X86_64 Unix methods from X86_64 Unix.
 * </p>
 *
 * @author Matthew Wilson
 */
public final class X86_64UnixNativeCallFactory extends BaseNativeCallFactory
{

    private static final DynamicLibrary LIBRARY;

    /** JNI function for <=3 integer parameters. */
    private static final Pointer CALL_INT_3;

    /** JNI function for 4 integer parameters. */
    private static final Pointer CALL_INT_4;

    /** JNI function for 5 integer parameters. */
    private static final Pointer CALL_INT_5;

    /** JNI function for >=6 integer parameters. */
    private static final Pointer CALL_INT_6;

    /** JNI function for reading errno. */
    private static final Pointer CALL_ERRNO;


    static
    {
        LIBRARY = DniInternal.PLATFORM.getDniDynamicLibrary();
        try
        {
            CALL_INT_3 = LIBRARY.getSymbol( "dni_x86_64_unix_call_int_3" );
            CALL_INT_4 = LIBRARY.getSymbol( "dni_x86_64_unix_call_int_4" );
            CALL_INT_5 = LIBRARY.getSymbol( "dni_x86_64_unix_call_int_5" );
            CALL_INT_6 = LIBRARY.getSymbol( "dni_x86_64_unix_call_int_6" );
            CALL_ERRNO = LIBRARY.getSymbol( "dni_x86_64_unix_call_errno" );
        }
        catch ( Throwable throwable )
        {
            try
            {
                LIBRARY.free();
            }
            catch ( Throwable throwable2 )
            {
                DniInternal.LOGGER.log( Level.SEVERE, "Failed to free library", throwable2 );
            }
            throw new ExceptionInInitializerError( throwable );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    Pointer getNativeBinding( final List< Parameter > returnParameterContainer,
                              final List< Parameter > parameters,
                              final Pointer functionPointer,
                              final LastErrorBehaviour lastErrorBehaviour )
    {
        // replace structures by value
        boolean done ;
        do
        {
            done = true;
            for ( int i = 0; i < parameters.size(); i++ )
            {
                Parameter parameter = parameters.get( i );
                if ( parameter instanceof StructureByValueParameter )
                {
                    parameters.remove( i );
                    parameters.addAll( i, StructureByValueOnStackParameter.create( (StructureByValueParameter) parameter ) );

                    done = false;
                    break;
                }
            }
        }
        while ( !done );

        // replace return structure by value
        Parameter returnParameter = returnParameterContainer.get( 0 );
        if ( returnParameter instanceof StructureByValueParameter )
        {
            // an "invisible" parameter is inserted into the function call which
            // is the pointer to the return value

            StructureRecord structureRecord = ( (StructureByValueParameter) returnParameter ).getStructureRecord();
            int size = structureRecord.getInfo().getSize();
            if ( size <= 0 )
            {
                throw new IllegalLibraryException( "Cannot return opaque structure by value: " + structureRecord.getType().getName() );
            }

            if ( size <= 8 )
            {
                // returned in a single 64-bit register
                returnParameterContainer.set( 0, new ReturnSmallStructureByValueParameter( structureRecord ) );
            }
            else if ( size <= 16 )
            {
                // TODO v1.0 Returning structs by value doesn't work for small structs
                // Calling convention passes the struct in rax:rcx (I think)
                // and this makes is hard to get from Java.
                throw new UnsupportedOperationException( "FIXME: Returning structs by value doesn't work for small structs" );
            }
            else
            {
                // pointer to a structure slot is passed as a hidden parameter
                ReturnStructureByValueParameter parameter = new ReturnStructureByValueParameter( structureRecord );
                parameters.add( 0, parameter );
                returnParameterContainer.set( 0, parameter );
            }

        }

        Pointer nativeBinding;
        if ( lastErrorBehaviour == LastErrorBehaviour.IGNORE )
        {
            nativeBinding = handleStandardCase( parameters, functionPointer );
        }
        else
        {
            nativeBinding = handleLastErrorCase( parameters, functionPointer );
        }

        return nativeBinding;
    }

    /**
     * Handles the standard (non-error) case.
     *
     * @param parameters
     *            the parameters
     * @param functionPointer
     *            the function pointer
     * @return the native binding
     */
    private static Pointer handleStandardCase( final List< Parameter > parameters,
                                               final Pointer functionPointer )
    {
        // how many integer parameters are there?
        int intCount = 0;
        for ( Parameter parameter : parameters )
        {
            if ( parameter.isInteger() )
            {
                intCount++;
            }
        }

        Pointer nativeBinding;
        if ( intCount == 0 )
        {
            // no mangling of parameters to be done;
            // actually point directly to native function (and not pass function
            // pointer as a parameter!)
            nativeBinding = functionPointer;
        }
        else if ( intCount <= 3 )
        {
            // int parameters have to be shuffled by native code
            nativeBinding = CALL_INT_3;

            // have to pass the function pointer
            parameters.add( 0, new FunctionPointerParameter( functionPointer ) );
        }
        else
        {
            // have to mangle the parameters a bit:
            nativeBinding = mangleIntSpill( parameters, functionPointer, intCount );
        }

        return nativeBinding;
    }

    /**
     * <p>
     *  Mangles the parameters where there are more than three integers.
     * </p>
     *
     * @param parameters
     *            the parameters
     * @param functionPointer
     *            the function pointer
     * @param intCount
     *            the number of integer arguments
     *
     * @return the native binding
     */
    private static Pointer mangleIntSpill( final List< Parameter > parameters,
                                           final Pointer functionPointer,
                                           final int intCount )
    {
        // locate the first six int parameters
        List< Parameter > intParameters = new ArrayList< Parameter >();
        int displacementOffset = 8;
        for ( Parameter parameter : parameters )
        {
            if ( parameter.isInteger() && intParameters.size() < 6 )
            {
                intParameters.add( parameter );
            }
            else
            {
                displacementOffset += 8;
            }
        }

        // always have to pass the function pointer (%rdx)
        parameters.add( 0, new FunctionPointerParameter( functionPointer ) );

        // and the offset to the displaced arguments (%rcx)
        parameters.add( 1, new ConstantLongParameter( displacementOffset ) );

        // these have to be reinserted in the right places
        parameters.removeAll( intParameters );

        if ( intCount < 4 )
        {
            throw new AssertionError();
        }

        if ( intCount == 4 )
        {
            // Since four are already taken, no integer arguments can match
            // their destination slots; all have to be displaced.
            // The first two can be kept in registers (to be moved by native
            // code) and the other two end up on the end of the stack.
            assert intParameters.size() == 4;

            // %r8 (to be moved to %rdi)
            parameters.add( 2, intParameters.get( 0 ) );

            // %r9 (to be moved to %rsi)
            parameters.add( 3, intParameters.get( 1 ) );

            parameters.add( intParameters.get( 2 ) );
            parameters.add( intParameters.get( 3 ) );

            return CALL_INT_4;
        }

        if ( intCount == 5 )
        {
            // Since four are already taken, only one integer arguments can
            // match its destination slot; all others have to be displaced.
            // The first two can be kept in registers (to be moved by native
            // code) and the other two end up on the end of the stack.

            // %r8 (already correct)
            parameters.add( 2, intParameters.get( 4 ) );

            // %r9 (to be moved to %rdi)
            parameters.add( 3, intParameters.get( 0 ) );

            parameters.add( intParameters.get( 1 ) );
            parameters.add( intParameters.get( 2 ) );
            parameters.add( intParameters.get( 3 ) );

            return CALL_INT_5;
        }

        // Now two can match their destination slots.

        // %r8 (already correct)
        parameters.add( 2, intParameters.get( 4 ) );

        // %r9 (already correct)
        parameters.add( 3, intParameters.get( 5 ) );

        parameters.add( intParameters.get( 0 ) );
        parameters.add( intParameters.get( 1 ) );
        parameters.add( intParameters.get( 2 ) );
        parameters.add( intParameters.get( 3 ) );

        return CALL_INT_6;
    }

    /**
     * Handles the standard (non-error) case.
     *
     * @param parameters
     *            the parameters
     * @param functionPointer
     *            the function pointer
     * @return the native binding
     */
    private static Pointer handleLastErrorCase( final List< Parameter > parameters,
                                                final Pointer functionPointer )
    {
        // At present, we use a very similar stub as the CALL_INT_6 stub.  It's
        // less efficient, but anything that can set errno is usually going to
        // be a lot slower anyway.

        // locate the first six int parameters
        List< Parameter > intParameters = new ArrayList< Parameter >();
        int displacementOffset = 8;
        for ( Parameter parameter : parameters )
        {
            if ( parameter.isInteger() && intParameters.size() < 6 )
            {
                intParameters.add( parameter );
            }
            else
            {
                displacementOffset += 8;
            }
        }

        // create some fake integer parameters
        while ( intParameters.size() < 6 )
        {
            intParameters.add( new PaddingParameter( 'J' ) );
        }

        // always have to pass the function pointer (%rdx)
        parameters.add( 0, new FunctionPointerParameter( functionPointer ) );

        // and the offset to the displaced arguments (%rcx)
        parameters.add( 1, new ConstantLongParameter( displacementOffset ) );

        // these have to be reinserted in the right places
        parameters.removeAll( intParameters );

        // %r8 (already correct)
        parameters.add( 2, intParameters.get( 4 ) );

        // %r9 (already correct)
        parameters.add( 3, intParameters.get( 5 ) );

        parameters.add( intParameters.get( 0 ) );
        parameters.add( intParameters.get( 1 ) );
        parameters.add( intParameters.get( 2 ) );
        parameters.add( intParameters.get( 3 ) );

        parameters.add( new LastErrorSlotParameter() );
        parameters.add( new PaddingParameter( 'J' ) ); // for preserving values
        parameters.add( new PaddingParameter( 'J' ) ); // for preserving values

        return CALL_ERRNO;
    }

}
